package com.secure.support.resttemplate;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.secure.bean.AppKey;
import com.secure.bean.ClientAppKey;
import com.secure.bean.UserKey;
import com.secure.constant.RequestConstant;
import com.secure.constant.SeparatorConstant;
import com.secure.exception.ApiSecureException;
import com.secure.handler.EncryptDecryptHandler;
import com.secure.handler.SignatureHandler;
import com.secure.properties.SecureClientProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.AbstractClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Slf4j
public class RestRequestSecureInterceptor implements ClientHttpRequestInterceptor {

    /**
     * RestTemplate请求头ApiSecure，开启安全调用
     * 格式：serviceName=自定义服务名;decryptResponse=true
     * decryptResponse非必填，不填-取配置文件该配置；true-解密响应，false-不解密响应，覆盖配置文件该配置
     */
    private final static String API_SECURE_REQUEST_HEADER = String.format("^\\s*serviceName\\s*=\\s*(?<%s>[^;\\s]+){1}(\\s*;\\s*decryptResponse\\s*=\\s*(?<%s>(true|false)){1})?\\s*$", RequestConstant.SERVICE_NAME, RequestConstant.DECRYPT_RESPONSE);
    private final SecureClientProperties secureClientProperties;
    private final SignatureHandler signatureHandler;
    private final EncryptDecryptHandler encryptDecryptHandler;
    private UserKey userKey;
    private ClientAppKey appKey;

    public RestRequestSecureInterceptor(SecureClientProperties secureClientProperties, SignatureHandler signatureHandler, EncryptDecryptHandler encryptDecryptHandler) {
        this.secureClientProperties = secureClientProperties;
        this.signatureHandler = signatureHandler;
        this.encryptDecryptHandler = encryptDecryptHandler;
    }

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        if (isNeedSecureRequest(request)) {
            ClientHttpResponse response = secureRequest(request, execution);
            if (HttpStatus.OK == response.getStatusCode() && isNeedDecryptResponse(request)) {
                InputStream inputStream = response.getBody();
                if (inputStream.available() > 0) {
                    String responseStr = IoUtil.read(inputStream, StandardCharsets.UTF_8);
                    String plainText = encryptDecryptHandler.decryptByPublicKey(responseStr, appKey.getEncryptDecrypt().getPublicKey());
                    response = new RestClientHttpResponse(response, plainText.getBytes(StandardCharsets.UTF_8));
                }
            }
            return response;
        } else {
            return execution.execute(request, body);
        }
    }

    /**
     * 判断是否需要安全请求
     *
     * @param request 请求对象
     * @return
     */
    public boolean isNeedSecureRequest(HttpRequest request) {
        List<String> apiSecureHeaderList = request.getHeaders().get(RequestConstant.API_SECURE);
        if (CollectionUtil.isNotEmpty(apiSecureHeaderList)) {
            Pattern pattern = Pattern.compile(API_SECURE_REQUEST_HEADER);
            Matcher matcher = pattern.matcher(apiSecureHeaderList.get(0));
            if (matcher.find()) {
                String serviceName = matcher.group(RequestConstant.SERVICE_NAME);
                if (StrUtil.isBlank(serviceName)) {
                    throw new ApiSecureException("消费方：RestTemplate请求头ApiSecure需要指定服务名");
                }
                ClientAppKey appKey = secureClientProperties.getAppKeyConfig().get(serviceName);
                UserKey userKey = secureClientProperties.getUserKey();
                validateSecureConfig(userKey, appKey);
                this.appKey = appKey;
                this.userKey = userKey;
                return true;
            }
        }
        return false;
    }

    /**
     * 校验安全配置
     *
     * @param userKey 服务消费方密钥配置
     * @param appKey  服务提供方密钥配置
     * @return
     */
    private void validateSecureConfig(UserKey userKey, AppKey appKey) {
        if (ObjectUtil.isNull(userKey)) {
            throw new ApiSecureException("消费方：消费方安全配置未配置");
        }
        if (StrUtil.isBlank(userKey.getUserId())) {
            throw new ApiSecureException("消费方：消费方标识未配置");
        }
        if (ObjectUtil.isNull(userKey.getSignature()) || StrUtil.isBlank(userKey.getSignature().getPrivateKey())) {
            throw new ApiSecureException("消费方：消费方签名私钥未配置");
        }
        if (ObjectUtil.isNull(appKey)) {
            throw new ApiSecureException("消费方：服务方安全配置未配置");
        }
        if (StrUtil.isBlank(appKey.getAppId())) {
            throw new ApiSecureException("消费方：服务方标识未配置");
        }
        if (ObjectUtil.isNull(appKey.getEncryptDecrypt()) || StrUtil.isBlank(appKey.getEncryptDecrypt().getPublicKey())) {
            throw new ApiSecureException("消费方：服务方提供的加密请求/解密响应的公钥未配置");
        }
    }

    /**
     * 安全请求
     *
     * @param request          请求对象
     * @param requestExecution 请求执行器
     * @return
     * @throws IOException
     */
    private ClientHttpResponse secureRequest(HttpRequest request, ClientHttpRequestExecution requestExecution) throws IOException {
        ClientHttpResponse httpResponse;
        MediaType mediaType = request.getHeaders().getContentType();
        if (ObjectUtil.isNotNull(mediaType) && !MediaType.ALL.equals(mediaType)) {
            ByteArrayOutputStream outputStream = (ByteArrayOutputStream) ((AbstractClientHttpRequest) request).getBody();
            String requestParams = new String((outputStream).toByteArray());
            String contentType = mediaType.toString();
            byte[] secureRequestBytes;
            if (contentType.contains(MediaType.APPLICATION_JSON_VALUE) || contentType.contains(MediaType.TEXT_PLAIN_VALUE)) {
                secureRequestBytes = convertRequestParamsForBody(requestParams, appKey);
            } else if (contentType.contains(MediaType.MULTIPART_FORM_DATA_VALUE)) {
                secureRequestBytes = convertRequestParamsForForm(requestParams, contentType.split(";boundary=")[1], appKey);
            } else if (contentType.contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) {
                secureRequestBytes = convertRequestParamsForXWwwFormUrlencoded(requestParams, appKey);
            } else {
                throw new ApiSecureException("安全请求暂不支持请求类型" + mediaType);
            }
            outputStream.reset();
            outputStream.write(secureRequestBytes);
            httpResponse = requestExecution.execute(request, secureRequestBytes);
        } else {
            URI newUri = convertRequestUrlForQuery(request.getURI(), appKey);
            RestClientHttpRequest restClientHttpRequest = new RestClientHttpRequest(request, newUri);
            restClientHttpRequest.getHeaders().add(RequestConstant.DECODE_REQUEST, "true");
            httpResponse = restClientHttpRequest.execute();
        }

        return httpResponse;
    }

    /**
     * 转换请求参数(form表单传参，multipart/form-data)
     *
     * @param requestParams 请求参数
     * @param appKey        安全配置
     * @return
     * @throws UnsupportedEncodingException
     */
    private byte[] convertRequestParamsForForm(String requestParams, String boundary, AppKey appKey) {
        //1.构造map格式入参
        Map<String, Object> requestMap = new HashMap<>();
        List<String> paramList = Arrays.stream(requestParams.split("--".concat(boundary))).filter(item -> item.contains("Content-Disposition: form-data; name=\"")).collect(Collectors.toList());
        if (CollectionUtil.isNotEmpty(paramList)) {
            for (String param : paramList) {
                String[] paramItem = param.split("\r\n");
                String paramName = paramItem[1].substring("Content-Disposition: form-data; name=".length() + 1, paramItem[1].length() - 1);
                String paramValue = paramItem[paramItem.length - 1];
                requestMap.put(paramName, paramValue);
            }
        }
        log.info("原请求参数(map格式)：{}", requestMap);

        //2.签名，并且将签名加进map格式原请求参数
        String signature = signatureHandler.sign(requestMap, userKey.getSignature().getPrivateKey());
        requestMap.put(RequestConstant.SIGNATURE, signature);
        log.info("原请求参数加签：{}", requestMap);

        //3.对原请求参数加密
        String encText = encryptDecryptHandler.encryptByPublicKey(requestMap, appKey.getEncryptDecrypt().getPublicKey());

        //4.构造安全请求入参
        StringBuilder secureRequestStr = new StringBuilder();
        secureRequestStr.append("--").append(boundary).append("\r\n").append("Content-Disposition: form-data; name=\"").append(RequestConstant.APPID).append("\"\r\n").append("Content-Type: text/plain;charset=UTF-8\r\n").append("\r\n").append(appKey.getAppId()).append("\r\n").append("--").append(boundary).append("\r\n").append("Content-Disposition: form-data; name=\"").append(RequestConstant.USERID).append("\"\r\n").append("Content-Type: text/plain;charset=UTF-8\r\n").append("\r\n").append(userKey.getUserId()).append("\r\n").append("--").append(boundary).append("\r\n").append("Content-Disposition: form-data; name=\"").append(RequestConstant.TIMESTAMP).append("\"\r\n").append("Content-Type: application/json\r\n").append("\r\n").append(Instant.now().getEpochSecond()).append("\r\n").append("--").append(boundary).append("\r\n").append("Content-Disposition: form-data; name=\"").append(RequestConstant.DATA).append("\"\r\n").append("Content-Type: text/plain;charset=UTF-8\r\n").append("Content-Length: ").append(encText.length()).append("\r\n").append("\r\n").append(encText).append("\r\n").append("--").append(boundary).append("--\r\n");

        return secureRequestStr.toString().getBytes(StandardCharsets.UTF_8);
    }

    /**
     * 转换请求参数(form表单传参，application/x-www-form-urlencoded)
     *
     * @param requestParams 请求参数
     * @param appKey        安全配置
     * @return
     * @throws UnsupportedEncodingException
     */
    private byte[] convertRequestParamsForXWwwFormUrlencoded(String requestParams, AppKey appKey) throws UnsupportedEncodingException {
        //1.构造map格式入参
        Map<String, Object> requestMap = new HashMap<>();
        if (StrUtil.isNotBlank(requestParams)) {
            String[] params = requestParams.split(SeparatorConstant.AMPERSAND);
            for (String param : params) {
                String[] paramItem = param.split(SeparatorConstant.EQUAL);
                requestMap.put(paramItem[0], paramItem.length > 1 ? URLDecoder.decode(paramItem[1], StandardCharsets.UTF_8.toString()) : null);
            }
        }
        log.info("原请求参数(map格式)：{}", requestMap);

        //2.签名，并且将签名加进map格式原请求参数
        String signature = signatureHandler.sign(requestMap, userKey.getSignature().getPrivateKey());
        requestMap.put(RequestConstant.SIGNATURE, signature);
        log.info("原请求参数加签：{}", requestMap);

        //3.对原请求参数加密
        String encText = encryptDecryptHandler.encryptByPublicKey(requestMap, appKey.getEncryptDecrypt().getPublicKey());

        //4.构造安全请求入参
        StringBuilder secureRequestStr = new StringBuilder();
        secureRequestStr.append(RequestConstant.USERID).append(SeparatorConstant.EQUAL).append(URLEncoder.encode(userKey.getUserId(), StandardCharsets.UTF_8.toString())).append(SeparatorConstant.AMPERSAND).append(RequestConstant.APPID).append(SeparatorConstant.EQUAL).append(URLEncoder.encode(appKey.getAppId(), StandardCharsets.UTF_8.toString())).append(SeparatorConstant.AMPERSAND).append(RequestConstant.TIMESTAMP).append(SeparatorConstant.EQUAL).append(Instant.now().getEpochSecond()).append(SeparatorConstant.AMPERSAND).append(RequestConstant.DATA).append(SeparatorConstant.EQUAL).append(URLEncoder.encode(encText, StandardCharsets.UTF_8.toString()));
        return secureRequestStr.toString().getBytes(StandardCharsets.UTF_8);
    }

    /**
     * 转换请求参数(body请求体传参)
     *
     * @param requestParams 请求参数
     * @param appKey        安全配置
     * @return
     */
    private byte[] convertRequestParamsForBody(String requestParams, AppKey appKey) {
        //1.构造map格式入参
        Map<String, Object> requestMap;
        if (JSONUtil.isTypeJSONObject(requestParams)) {
            // 原请求参数已经是map，这里不需要构造
            requestMap = JSONUtil.toBean(requestParams, HashMap.class);
        } else {
            //请求体是字符串，构造map格式入参约定使用固定键stringBody
            requestMap = new HashMap<>();
            requestMap.put(RequestConstant.STRING_BODY, requestParams);
        }
        log.info("原请求参数(map格式)：{}", requestMap);

        //2.签名，并且将签名加进原请求参数
        String signature = signatureHandler.sign(requestMap, userKey.getSignature().getPrivateKey());
        requestMap.put(RequestConstant.SIGNATURE, signature);
        log.info("原请求参数加签：{}", requestMap);

        //3.对原请求参数加密
        String encText = encryptDecryptHandler.encryptByPublicKey(requestMap, appKey.getEncryptDecrypt().getPublicKey());

        //4.构造安全请求入参
        Map<String, Object> secureRequestMap = new HashMap<>(2);
        secureRequestMap.put(RequestConstant.USERID, userKey.getUserId());
        secureRequestMap.put(RequestConstant.APPID, appKey.getAppId());
        secureRequestMap.put(RequestConstant.TIMESTAMP, Instant.now().getEpochSecond());
        secureRequestMap.put(RequestConstant.DATA, encText);
        log.info("安全请求参数：{}", secureRequestMap);

        return JSONUtil.toJsonStr(secureRequestMap).getBytes(StandardCharsets.UTF_8);
    }

    /**
     * 转换请求url(query查询参数)
     *
     * @param originUri 原请求url
     * @param appKey    安全配置
     * @return
     * @throws UnsupportedEncodingException
     */
    private URI convertRequestUrlForQuery(URI originUri, AppKey appKey) throws UnsupportedEncodingException {
        String originUriQuery = originUri.getQuery();

        //构造请求参数
        Map<String, Object> requestMap = new HashMap<>();
        if (StrUtil.isNotBlank(originUriQuery)) {
            String[] params = originUriQuery.split(SeparatorConstant.AMPERSAND);
            for (String param : params) {
                String[] paramItem = param.split(SeparatorConstant.EQUAL);
                requestMap.put(paramItem[0], paramItem.length > 1 ? URLDecoder.decode(paramItem[1], StandardCharsets.UTF_8.toString()) : null);
            }
        }

        //2.签名，并且将签名加进原请求参数
        String signature = signatureHandler.sign(requestMap, userKey.getSignature().getPrivateKey());
        requestMap.put(RequestConstant.SIGNATURE, signature);
        log.info("原请求参数加签：{}", requestMap);

        //3.对原请求参数加密
        String encText = encryptDecryptHandler.encryptByPublicKey(requestMap, appKey.getEncryptDecrypt().getPublicKey());

        //4.构造安全请求入参
        StringBuilder secureRequestQuery = new StringBuilder();
        secureRequestQuery.append(RequestConstant.USERID).append(SeparatorConstant.EQUAL).append(URLEncoder.encode(userKey.getUserId(), StandardCharsets.UTF_8.toString())).append("&");
        secureRequestQuery.append(RequestConstant.APPID).append(SeparatorConstant.EQUAL).append(URLEncoder.encode(appKey.getAppId(), StandardCharsets.UTF_8.toString())).append("&");
        secureRequestQuery.append(RequestConstant.TIMESTAMP).append(SeparatorConstant.EQUAL).append(Instant.now().getEpochSecond()).append("&");
        secureRequestQuery.append(RequestConstant.DATA).append(SeparatorConstant.EQUAL).append(URLEncoder.encode(encText, StandardCharsets.UTF_8.toString()));
        log.info("安全请求参数：{}", secureRequestQuery);

        try {
            return new URI(originUri.getScheme(), originUri.getAuthority(), originUri.getPath(), secureRequestQuery.toString(), originUri.getFragment());
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 判断响应是否需要解密
     *
     * @param request 请求对象
     * @return
     */
    private boolean isNeedDecryptResponse(HttpRequest request) {
        boolean isNeedDecryptResponse = appKey.isDecryptResponse();
        List<String> apiSecureHeaderList = request.getHeaders().get(RequestConstant.API_SECURE);
        if (CollectionUtil.isNotEmpty(apiSecureHeaderList)) {
            Pattern pattern = Pattern.compile(API_SECURE_REQUEST_HEADER);
            Matcher matcher = pattern.matcher(apiSecureHeaderList.get(0));
            if (matcher.find()) {
                String decryptResponse = matcher.group(RequestConstant.DECRYPT_RESPONSE);
                if (StrUtil.isNotBlank(decryptResponse)) {
                    isNeedDecryptResponse = "true".equals(decryptResponse);
                }
            }
        }
        return isNeedDecryptResponse;
    }
}
